Transactions

Transactional transparency

DataObjects.Net is built to simplify writing business code – a code written on high-level language (e.g. C#) and operating on application server.

This brings some new effects in comparison to the case developers faced earlier, when similar code was written in stored procedures:

  • There is an ORM transforming relational data to high-level language objects. Normally presence of ORM implies significant differences in behavior: e.g. if you remove the row in database, this happens immediately – and it’s very convenient to rely on sequence of operations there. But many ORM tools delay actual operations until explicit invocation of Session.Flush() or similar method – so basically, you can’t be sure the state you see is the same as the state in RDBMS. May be the closest analogue of this is WYSIWYG principal, and here it’s broken.
  • Frequently application server is communicating with RDBMS over network, so developers must care about reducing the chattiness between RDBMS and application server.

DataObjects.Net addresses both of these issues:

  • Transparent persistence and transactional transparency ensure you’re working with entities as if they’re “directly connected” to the underlying database objects:

    • On any reads, you get exactly the same values that must be retrieved from the database right directly. So if you just read something, direct SQL query executed on the same connection & transaction, and retrieving the same data directly, would return exactly the same value.
    • Any cached changes are persisted before any data retrievals.
    • Transaction rollbacks are immediately reflected in state. There is no necessity to update anything manually.
    • Any cached state is considered as stale on crossing the transaction boundary. For simplicity, you can imply DataObjects.Net simply invalidates it.
    • And so on. The final goal is to simulate the “direct connectivity”, but cache as much as possible without breaking this to reduce the chattiness.

    So DataObjects.Net provides “full WYSIWYG” here.

  • Generalized batching, future queries and prefetch API allow you to dramatically reduce the chattiness with almost zero efforts.

    Note

    Transparent persistence and transactional transparency allows developer to be sure that he always deal with actual state: the state he sees is absolutely the same as direct SQL queries performed on the same connection & transaction would show.

For example, if you’re fetching some entity in transaction TA and trying to get its property value in transaction TB, by default its value would be re-fetched there. “By default” indicates there are ways to make DataObjects.Net behave differently, but you must do this explicitly (e.g. you can use DisconnectedState or global cache – they’ll be described further).

Note

As a consequence, DataObjects.Net requires any data access operations to be executed inside logical transactions. “Logical” here means that these transactions define isolation boundaries just for DataObjects.Net. They can be bound to “physical” transactions executed on RDBMS level, or can not – e.g. logical transactions aren’t bound to physical ones when DisconnectedState is attached to Session.

Manual transactions

To open a transaction manually, you should call static session.OpenTransaction(...) method (it has several overloads):

using (var session = domain.OpenSession()) {
  using (var transactionScope = session.OpenTransaction()) {

    var person = new Person();
    person.Name = "Barack Obama";

    transactionScope.Complete();
  }
}

The simplest form of this method opens a transaction in active session and returns its transaction scope – an IDisposable object used to committing or rolling back the transaction.

  • To commit the transaction, call transactionScope.Complete() method and dispose the transaction scope.

  • To rollback the transaction, just dispose the transaction scope without calling Complete() method.

    Note

    In example above we open a transaction inside using block, so transaction scope returned by Open() method will be definitely disposed on leaving this block. If code inside using block throws an exception, Complete() method won’t be invoked. So our transaction will be committed only if code inside it is executed successfully.

If session.OpenTransaction(...) method is invoked, but active session already has associated transaction, it does nothing and return so-called void scope. Such a scope does nothing on Complete() method call and its disposal as well.

Note

It’s possible to open a nested transaction – e.g. by invoking session.OpenTransaction(TransactionOpenMode.New); such a call never returns a void scope. You’ll find the information on nested transactions further.

You can also open a transaction with specified isolation level: session.OpenTransaction(IsolationLevel isolationLevel).

Automatic transactions

Another way to ensure a particular operation sequence is performed inside a transaction is to use automatic transactions. Automatic transactions are actually provided by TransactionalAspect (PostSharp aspect) looking up for [Transactional] attribute, that can be applied to any instance method, property accessor or constructor of SessionBound descendants (e.g. entities, structures, entity sets or services).

When [Transactional] attribute is applied to a particular method, all operations inside it are wrapped into a transaction:

[Transactional]
public void DoSomething()
{
  // ...
}

When this method is compiled, PostSharp rewrites its body to nearly the following one:

public void DoSomething()
{
  using (var transactionScope = Session.OpenTransaction()) {

    // ...

    transactionScope.Complete();
  }
}

**Note**

By default DataObjects.Net applies ``[Transactional]`` to all public
instance methods, property accessors and constructors of
``SessionBound`` descendants. To suppress such behavior on a
particular method, apply ``Transactional(false)]`` to it. So you
might notice the default behavior of ``[Transactional]`` attribute
is almost the same as for ``[ActivateSession]`` attribute.

Nested transactions

DataObjects.Net fully supports nested transactions, both manual and automatic:

  • Use session.OpenTransaction(TransactionOpenMode.New) to manually open a new transaction. If there is no outermost transaction in the active Session, the outermost transaction will be opened; otherwise a nested one will be opened.
  • Use [Transactional(TransactionOpenMode.New)] to require a new automatic transaction for a particular method.

Nesting level is limited only by underlying RDBMS.

// Building the domain
var domain = BuildDomain();

using (var session = domain.OpenSession()) {
  using (var transactionScope = session.OpenTransaction()) {

    // Creating user
    var dmitri = new User {
      Name = "Dmitri"
    };

    // Modifying the entity
    dmitri.Name = "Dmitri Maximov";

    // Opening new nested transaction
    using (var nestedScope = session.OpenTransaction(TransactionOpenMode.New)) {
      // Removing the entity
      dmitri.Remove();
      Assert.IsTrue(dmitri.IsRemoved);
      AssertEx.Throws<InvalidOperationException>(() => {
        var dmitryName = dmitri.Name;
      });
      // No nestedScope.Complete(), so nested transaction will be rolled back
    }

    // Transparent Entity state update
    Assert.IsFalse(dmitri.IsRemoved);
    Assert.AreEqual("Dmitri Maximov", dmitri.Name);

    // Repeating the same using transactional method
    AssertEx.Throws<InvalidOperationException>(() => {
      dmitri.RemoveAndCancel();
    });

    // Transparent Entity state update
    Assert.IsFalse(dmitri.IsRemoved);
    Assert.AreEqual("Dmitri Maximov", dmitri.Name);

    // Marking the transaction scope as completed to commit it
    transactionScope.Complete();
  }
}
[Transactional(TransactionOpenMode.New)]
public void RemoveAndCancel()
{
  Remove();
  throw new InvalidOperationException("Cancelled.");
}